<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage core
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

use FB;

/**
 * The Logger class provides an interface to a simple logging mechanism. Currently the logging mechanism only supports logging
 * through FirePHP
 *
 * @author Michiel Hakvoort
 * @version 1.0
 * @abstract
 * @
 */
abstract class Logger {

    // Log levels
    const CONFIG	= 0x01;
    const INFO		= 0x02;
    const WARNING	= 0x04;
    const SEVERE	= 0x08;

    const EXCEPTION	= 0x10;

    const DUMP		= 0x20;

    const ALL		= 0x1F;


    private $name = null;
    private $level = null;

    /**
     * Create a new mgLogger with the specified name and the log level at {@link mgLogger :: ALL} ^ {@link mgLogger :: CONFIG}.
     *
     * @access protected
     * @param string $name The name of the mgLogger
     */
    public function __construct($name) {
        $this->name = $name;
        $this->level = self :: ALL ^ self :: CONFIG;
    }

    /**
     * Get the name of the mgLogger.
     *
     * @access public
     * @return string The name of the mgLogger
     */
    public function getName() {
        return $this->name;
    }

    /**
     * Set the loglevel of the mgLogger.
     *
     * @access public
     * @param int $level
     */
    public function setLevel($level) {
        $level = $level & self :: ALL;
        $this->level = $level;
    }

    public function c($message) {
        if(!($this->level & self :: CONFIG)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: CONFIG, $message);
    }

    /**
     * Log a config message.
     *
     * @access public
     * @param string $message
     */
    public function config($message) {
        if(!($this->level & self :: CONFIG)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: CONFIG, $message);
    }

    public function i($message) {
        if(!($this->level & self :: INFO)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: INFO, $message);
    }

    /**
     * Log an info message.
     *
     * @access public
     * @param string $message
     */
    public function info($message) {
        if(!($this->level & self :: INFO)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: INFO, $message);
    }

    public function l($message) {
        if(!($this->level & self :: INFO)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: INFO, $message);
    }
    /**
     * Log an info message.
     *
     * @access public
     * @param string $message
     */
    public function log($message) {
        if(!($this->level & self :: INFO)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: INFO, $message);
    }

    public function e(Exception $exception) {
        $this->performLog(self :: EXCEPTION, $exception);
    }

    /**
     * Log an Exception.
     *
     * @access public
     * @param Exception $exception
     */
    public function exception(Exception $exception) {
        $this->performLog(self :: EXCEPTION, $exception);
    }

    public function w($message) {
        if(!($this->level & self :: WARNING)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: WARNING, $message);
    }

    /**
     * Log a warning message.
     *
     * @access public
     * @param string $message
     */
    public function warning($message) {
        if(!($this->level & self :: WARNING)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: WARNING, $message);
    }

    public function s($message) {
        if(!($this->level & self :: SEVERE)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: SEVERE, $message);
    }

    /**
     * Log a severe message.
     *
     * @access public
     * @param string $message
     */
    public function severe($message) {
        if(!($this->level & self :: SEVERE)) {
            return;
        }

        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        $this->performLog(self :: SEVERE, $message);
    }

    /**
     * Dump an object in the log.
     *
     * @access public
     * @param mixed $objectOrName
     * @param object $object
     */
    public abstract function dump($objectOrName, $object = null);

    /**
     * Perform a log action.
     *
     * @access protected
     * @param int $level The log level.
     * @param string $message The message to log.
     */
    public abstract function performLog($level, $message);
}

/**
 * A {@link Logger} which logs into nothing.
 *
 * @author Michiel Hakvoort
 * @version 1.0
 */
class NullLogger extends Logger {

    /**
     * Construct the mgNullLogger named 'null'
     */
    public function __construct() {
        parent :: __construct('null');
    }

    /**
     * {@inheritDoc}
     */
    public function performLog($level, $message) {

    }

    public function dump($objectOrName, $object = null) {

    }
}

class FileLogger extends Logger {

    private $fp = null;

    private $path = null;

    private static $reusableException = null;

    public function __construct($name, $folder) {
        parent :: __construct($name);

        if(self :: $reusableException === null) {
            self :: $reusableException = new \Exception();
        }
        $this->path = $folder;

        mkdirrwx($folder);

        $file = $folder . DS . $name . '.log';

        $this->fp = fopen($file, 'a+');
    }


    public function performLog($level, $message) {

        $relativeTime = number_format((microtime(true) ) - ((int)$_SERVER['REQUEST_TIME']), 3);
        $currentTime = date('H:i:s');

        $e = new \Exception();
        $trace = $e->getTrace();


        $offset = null;
        $callPoint = null;

        while(!empty($trace) && ($callPoint === null)) {
            $callPoint = array_shift($trace);

            if(!isset($callPoint['class']) || !isset($callPoint['function'])) {
                $callPoint = null;
                continue;
            }

            if(!(strtolower($callPoint['class']) === 'mg\log' && strtolower($callPoint['function']) !== 'performlog')) {
                $callPoint = null;
            }
        }

        if($callPoint === null) {
            Log :: warning('Logging must be called from \mg\Log class');
            return;
        }

        $file = $callPoint['file'];
        $line = $callPoint['line'];

        $path = dirname($file);

        $rootLength = null;

        // can become a bit cheaper
        for($i = 0, $c = min(mb_strlen($path), mb_strlen($this->path)); $i < $c && ($rootLength === null); $i++) {
            if(mb_substr($path, 0, $i) !== mb_substr($this->path, 0, $i)) {
                $rootLength = $i - 1;
            }
        }

        $file = substr($file, $rootLength);


        $entry = '[%1$s | %2$6.3f | %3$s:%4$d | %5$s] : %6$s';

        $l = '';
        switch($level) {
            case Logger :: CONFIG :
                $l = 'CONFIG';
                break;
            case Logger :: INFO :
                $l = 'INFO';
                break;
            case Logger :: WARNING :
                $l = 'WARNING';
                break;
            case Logger :: SEVERE :
                $l = 'SEVERE';
                break;
            case Logger :: EXCEPTION :
                $l = 'EXCEPTION';
                break;
            case Logger :: DUMP :
                $l = 'DUMP';
            default :
        }

        fwrite($this->fp, sprintf($entry, $currentTime, $relativeTime, $file, $line, $l, $message) . "\n");
    }

    public function __destruct() {
        if($this->fp !== null) {
            fclose($this->fp);
        }
    }

    /**
     * {@inheritDoc}
     */
    public function dump($objectOrName, $object = null) {
        return;
    }
}

/**
 * A {@link Logger} which sends the log to the browser using FirePHP.
 *
 * @author Michiel Hakvoort
 * @version 1.0
 */
class FirePHPLogger extends Logger {

    private static $isSetup = false;
    private static $isFlushed = false;

    private static $loggers = array();

    private $log = array();

    /**
     * Create a new mgFirePHPLogger with the specified name.
     *
     * @param string $name
     */
    public function __construct($name) {
        parent :: __construct($name);

        if(!self :: $isSetup) {
            self :: setup();
        }

        self :: $loggers[] = $this;

        // include lazy
        if(!class_exists('FB', false)) {
            include(__DIR__ . DS . '..' . DS . 'lib' . DS . 'FirePHPCore' . DS . 'fb.php');
        }
    }

    /**
     * Wrap an empty output buffer around the current output buffer. This way the logging information can be wrapped up, right
     * before the headers are sent.
     *
     * @access private
     * @static
     */
    private static function setup() {
        ob_start(array('mg\FirePHPLogger', 'clean'));

        self :: $isSetup = true;
    }

    /**
     * Write the logged messages to the client
     *
     * @access public
     * @static
     * @param string $line The line passed through
     * @return string The line passed through
     */
    public static function clean($line) {
        if(!headers_sent() && !self :: $isFlushed) {
            self :: $isFlushed = true;
            foreach(self :: $loggers as $logger) {
                $logger->flush();
            }
        }

        return $line;
    }

    /**
     * {@inheritDoc}
     */
    public function performLog($level, $message) {
        $time = number_format((microtime(true) ) - ((int)$_SERVER['REQUEST_TIME']), 3);

        $this->log[] = array($time, $level, $message);
    }

    /**
     * Flush the log
     *
     * @access private
     */
    private function flush() {
        if(count($this->log) === 0) {
            return;
        }

        FB :: group("Logger \"{$this->getName()}\"");

        while(count($this->log) > 0) {
            $line = array_shift($this->log);
            $time = $line[0];
            $level = $line[1];
            $message = $line[2];

            switch($level) {
                case Logger :: CONFIG :
                    FB :: log($time . 's : '. $message);
                    break;
                case Logger :: INFO :
                    FB :: info($time . 's : '. $message);
                    break;
                case Logger :: WARNING :
                    FB :: warn($time . 's : '. $message);
                    break;
                case Logger :: SEVERE :
                    FB :: error($time . 's : '. $message);
                    break;
                case Logger :: EXCEPTION :
                    FB :: error($message, $time.'s');
                default :
            }
        }
        FB :: groupEnd();
    }

    /**
     * {@inheritDoc}
     */
    public function dump($objectOrName, $object = null) {
        if($object == null) {
            FB :: dump('', $objectOrName);
        } else {
            FB :: dump($objectOrName, $object);
        }
    }
}


final class Log {

    private final function __construct() {

    }

    public static function c($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: CONFIG, $message);
    }

    /**
     * Log a config message.
     *
     * @access public
     * @param string $message
     */
    public static function config($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: CONFIG, $message);
    }

    public static function i($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: INFO, $message);
    }

    /**
     * Log an info message.
     *
     * @access public
     * @param string $message
     */
    public static function info($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: INFO, $message);
    }

    public static function l($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: INFO, $message);
    }

    public static function e(Exception $exception) {
        self :: performLog(Logger :: EXCEPTION, $exception);
    }

    /**
     * Log an Exception.
     *
     * @access public
     * @param Exception $exception
     */
    public static function exception(Exception $exception) {
        self :: performLog(Logger :: EXCEPTION, $exception);
    }

    public static function w($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: WARNING, $message);
    }

    /**
     * Log a warning message.
     *
     * @access public
     * @param string $message
     */
    public static function warning($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: WARNING, $message);
    }

    public static function s($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: SEVERE, $message);
    }

    /**
     * Log a severe message.
     *
     * @access public
     * @param string $message
     */
    public static function severe($message) {
        $args = func_get_args();

        if(count($args) > 1) {
            array_shift($args);
            $message = vsprintf($message, $args);
        }

        self :: performLog(Logger :: SEVERE, $message);
    }

    public static function dump($objectOrName, $object = null) {
        if($object !== null) {
            $message = $objectOrName . "\n" . print_r($object, true);
        } else {
            $message = print_r($objectOrName, true);
        }

        self :: performLog(Logger :: DUMP, $message);
    }

    private static function performLog($message, $level) {
        in(function(Logger $logger) use($message, $level) {
            $logger->performLog($message, $level);
        });
    }


}
